package at.donalfonso.kellner;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.mozartspaces.capi3.CoordinationData;
import org.mozartspaces.capi3.CountNotMetException;
import org.mozartspaces.capi3.FifoCoordinator;
import org.mozartspaces.capi3.KeyCoordinator;
import org.mozartspaces.capi3.LabelCoordinator;
import org.mozartspaces.capi3.LindaCoordinator;
import org.mozartspaces.capi3.Selector;
import org.mozartspaces.core.Capi;
import org.mozartspaces.core.CapiUtil;
import org.mozartspaces.core.Entry;
import org.mozartspaces.core.MzsCoreException;
import org.mozartspaces.core.MzsConstants.RequestTimeout;
import org.mozartspaces.core.MzsTimeoutException;
import org.mozartspaces.core.TransactionReference;
import org.mozartspaces.notifications.Notification;
import org.mozartspaces.notifications.NotificationListener;
import org.mozartspaces.notifications.Operation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import at.donalfonso.model.Bestellung;
import at.donalfonso.model.BestellungStatus;
import at.donalfonso.model.Gruppe;
import at.donalfonso.model.GruppeStatus;
import at.donalfonso.model.LieferBestellung;
import at.donalfonso.model.LieferBestellungStatus;
import at.donalfonso.model.LieferKunde;
import at.donalfonso.model.LieferKundeStatus;
import at.donalfonso.model.Pizza;
import at.donalfonso.model.Pizza.PizzaEnum;
import at.donalfonso.model.Tisch;
import at.donalfonso.model.Tisch.TischStatus;
import at.donalfonso.utility.SpaceUtility;

/**
 * Kellner
 * 
 * Interagiert mit folgenden Containern: Eingang, Saal, BestellungOffen, BestellungFertig
 * 
 */
public class Kellner implements NotificationListener {

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	private SpaceUtility space;
	private Capi capi;

	private String name;

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Kellner k1;
		if (args.length < 2) {
			System.out.println("Usage: java Kellner <ID> <Filiale>");
			Random random = new Random();
			k1 = new Kellner("K" + random.nextInt(100), "1");
		} 
		else {
			k1 = new Kellner(args[0], args[1]);
		}

		 k1.initNotification();
		
		 while (true) {
//			System.out.println(k1.getName() + " alive");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			try {
				k1.betreueEingang();
			}
			catch (CountNotMetException | MzsTimeoutException e) {
//				System.out.println("auch hier nichts zu tun...");
			}
			catch (MzsCoreException e) {
				e.printStackTrace();
			}
			try {
				k1.bestellungAufnehmen();
			}
			catch (CountNotMetException | MzsTimeoutException e) {
//				System.out.println("auch hier nichts zu tun...");
			}
			catch (MzsCoreException e) {
				e.printStackTrace();
			}
			try {
				k1.bestellungAusliefern();
			}
			catch (CountNotMetException | MzsTimeoutException e) {
//				System.out.println("auch hier nichts zu tun...");
			}
			catch (MzsCoreException e) {
				e.printStackTrace();
			}
			try {
				k1.bezahlen();
			}
			catch (CountNotMetException | MzsTimeoutException e) {
//				System.out.println("auch hier nichts zu tun...");
			}
			catch (MzsCoreException e) {
				e.printStackTrace();
			}
			try {
				k1.gruppeGegangen();
			}
			catch (CountNotMetException | MzsTimeoutException e) {
//				System.out.println("auch hier nichts zu tun...");
			}
			catch (MzsCoreException e) {
				e.printStackTrace();
			}

		 }
		
	}

	public Kellner(String name, String filiale) {

		space = new SpaceUtility(Integer.parseInt(filiale));
		capi = space.getCapi();
		
		this.name = name;
		System.out.println("Kellner: "+name + " (Filiale "+filiale+")");
	}
	
	/**
	 * Initialisiert die Notifikationen auf den Space-Containern.
	 */
	public void initNotification() {
		try {
			
			/* Der Kellner soll benachrichtigt werden, wenn Gruppen in einen neuen Container wechseln */
			space.getNotificationManager().createNotification(space.getEingangContainer(), this, Operation.WRITE);
			space.getNotificationManager().createNotification(space.getGruppeWillBestellenContainer(), this, Operation.WRITE);
			space.getNotificationManager().createNotification(space.getGruppeWillBezahlenContainer(), this, Operation.WRITE);
			space.getNotificationManager().createNotification(space.getGruppeGegangenContainer(), this, Operation.WRITE);
			space.getNotificationManager().createNotification(space.getTelefonContainer(), this, Operation.WRITE);
			
			/* Benachrichtigung über fertige Bestellungen */
			space.getNotificationManager().createNotification(space.getBestellungFertigContainer(), this, Operation.WRITE);
			

		} catch (MzsCoreException | InterruptedException e) {
			logger.debug("Kellner: Fehler beim initialisieren der Notifikationen.");
			e.printStackTrace();
		}
	}

	/**
	 * Name des Kellners.
	 * @return
	 */
	public String getName() {
		return name;
	}

	/**
	 * Weist wartenden Gästegruppen Tische zu.
	 */
	private void betreueEingang() throws MzsCoreException {

//		logger.debug("betreue Eingang");
		
		ArrayList<Gruppe> lg;
		ArrayList<Tisch> lt;
		TransactionReference tx = null;
		Gruppe gruppe = null;
		Tisch tisch = null;		

		tx = capi.createTransaction(2000, space.getUri());
			
		lg = capi.take(space.getEingangContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
		if (lg == null)
			return;
		gruppe = CapiUtil.getSingleEntry(lg);

		logger.debug("Gruppe gefunden: " + gruppe);
			
		if (gruppe != null) {
				
			/* Einen freien Tisch im Saal suchen */
			Tisch template = new Tisch();
			template.setStatus(TischStatus.FREI);

			lt = capi.take(space.getSaalContainer(), LindaCoordinator.newSelector(template), RequestTimeout.TRY_ONCE, tx);
			if (lt == null) {
				capi.rollbackTransaction(tx);
				return;
			}
			tisch = CapiUtil.getSingleEntry(lt);
				
			if (tisch != null) {
			
				logger.debug("Tisch gefunden: " + tisch);
				
				tisch.setGruppeId(gruppe.getId());
				tisch.setStatus(TischStatus.BESETZT);
				gruppe.setStatus(GruppeStatus.SITZEN);
				gruppe.setTischzuweisung(this.name);

				List<CoordinationData> coord1 =  Arrays.asList(LindaCoordinator.newCoordinationData(), KeyCoordinator.newCoordinationData(tisch.getId()));
				
				capi.write(new Entry(tisch, coord1), space.getSaalContainer(), RequestTimeout.TRY_ONCE, tx);
				capi.write(new Entry(gruppe, KeyCoordinator.newCoordinationData(gruppe.getId())), space.getGruppeSitzenContainer(), RequestTimeout.TRY_ONCE, tx);
	 			capi.commitTransaction(tx);
				logger.debug("Gruppe und Tisch geschrieben.");
			}
			else {
				logger.debug("Kein freier Tisch für Gruppe " + gruppe.getId());
				capi.rollbackTransaction(tx);
			}
		} 
		else {
			logger.debug("Keine Gruppe in Eingang gefunden.");
			capi.rollbackTransaction(tx);
		}
	}
	
	
	public void bestellungAufnehmen() throws MzsCoreException {
		
		logger.debug("Bestellung aufnehmen");
		
		TransactionReference tx = capi.createTransaction(2000, space.getUri());
			
		// Gruppe vom Space holen
		List<Gruppe> lg = capi.take(space.getGruppeWillBestellenContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
		Gruppe gruppe = CapiUtil.getSingleEntry(lg);
	
		if (gruppe != null) {
			List<PizzaEnum> pizzen = gruppe.getPizzen();
			
			// neue Bestellung vom Space holen
			
			List<Bestellung> lb = capi.take(space.getBestellungNeuContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
			Bestellung bestellung = CapiUtil.getSingleEntry(lb);
			bestellung.addPizzen(pizzen);
			
			// Zu jeder Bestellung wird die ID der Gruppe gespeichert.
			bestellung.setGruppenId(gruppe.getId());
			bestellung.setTischZuweisung(gruppe.getTischzuweisung());
			bestellung.setBestellKellner(this.name);

			capi.write(new Entry(bestellung, LindaCoordinator.newCoordinationData()), space.getBestellungOffenContainer(), RequestTimeout.TRY_ONCE, tx);
			logger.debug("Bestellung in BestellungOffen geschrieben: " + bestellung);

			gruppe.setStatus(GruppeStatus.BESTELLT);
			capi.write(new Entry(gruppe, KeyCoordinator.newCoordinationData(gruppe.getId())), space.getGruppeHatBestelltContainer(), RequestTimeout.TRY_ONCE, tx);
			logger.debug("Gruppe nach Bestellannahme zurückgeschrieben: " + gruppe);
			capi.commitTransaction(tx);
			
		} 
		else {
			logger.warn("Bestellannahme nicht möglich, keine Gruppe gefunden.");
			capi.rollbackTransaction(tx);
		}
	}
	
	public void telefonBestellungAufnehmen() throws MzsCoreException {
		
		logger.debug("telefonische Lieferbestellung aufnehmen");
		
		TransactionReference tx = capi.createTransaction(2000, space.getUri());
			
		// Gruppe vom Space holen
		List<LieferKunde> lk = capi.take(space.getTelefonContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
		LieferKunde kunde = CapiUtil.getSingleEntry(lk);
	
		if (kunde != null) {
			List<PizzaEnum> pizzen = kunde.getPizzen();
			
			// neue Bestellung vom Space holen
			
			List<Bestellung> lb = capi.take(space.getBestellungNeuContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
			Bestellung best = CapiUtil.getSingleEntry(lb);
			
			// Bestellung wird zur Lieferbestellung
			LieferBestellung bestellung = new LieferBestellung(best);
			
			bestellung.addPizzen(pizzen);
			
			// Zu jeder Bestellung wird die ID der Gruppe gespeichert.
			bestellung.setGruppenId(kunde.getId());
			bestellung.setAdresse(kunde.getAdresse());
			bestellung.setAdresseExistiert(kunde.adresseExistiert());
			bestellung.setBestellKellner(this.name);
			bestellung.setLieferstatus(LieferBestellungStatus.BESTELLT);
			
			capi.write(new Entry(bestellung, LindaCoordinator.newCoordinationData()), space.getBestellungOffenContainer(), RequestTimeout.TRY_ONCE, tx);
			logger.debug("Bestellung in BestellungOffen geschrieben: " + bestellung);

			kunde.setStatus(LieferKundeStatus.BESTELLT);

			capi.write(new Entry(kunde, KeyCoordinator.newCoordinationData(kunde.getId())), space.getSaalContainer(), RequestTimeout.TRY_ONCE, tx);
			logger.debug("Lieferkunde nach Bestellannahme zurückgeschrieben: " + kunde);
			capi.commitTransaction(tx);
			
		} 
		else {
			logger.warn("Bestellannahme nicht möglich, kein Lieferkunde gefunden.");
			capi.rollbackTransaction(tx);
		}
	}

	/**
	 * Liest die fertige Bestellung aus dem Container BestellungFertig und liest die zugehörigen
	 * Pizzen aus dem Container kueche_output.
	 */
	public void bestellungAusliefern() throws MzsCoreException {
		
		logger.debug("Bestellung wird ausgeliefert");
		
		Bestellung bestellung = null;
		
		Selector sel = LabelCoordinator.newSelector(Bestellung.class.getName());
		ArrayList<Serializable> bestellungen = capi.take(space.getBestellungFertigContainer(), sel, RequestTimeout.TRY_ONCE, null);
		if (bestellungen == null)
			return;
		bestellung = (Bestellung) CapiUtil.getSingleEntry(bestellungen);
		
		if (bestellung != null) {
		logger.info("Bestellung gelesen: "+bestellung);
		TransactionReference tx = capi.createTransaction(2000, space.getUri());
	
		for (Pizza p1: bestellung.getPizzen()) {
//			System.out.println("pizzaid: "+pizzaId);
//			ArrayList<Pizza> lpko = capi.take(getSpace().getKuecheOutputContainer(), KeyCoordinator.newSelector(p1.getId()), RequestTimeout.TRY_ONCE, tx);
			capi.delete(space.getKuecheOutputContainer(), KeyCoordinator.newSelector(p1.getId()), RequestTimeout.TRY_ONCE, tx);
//			System.out.println("Pizza gelöscht aus kueche_output.");
					
//			pizzen.add(CapiUtil.getSingleEntry(lp));
//			ArrayList<Pizza> lps = capi.take(getSpace().getSaalContainer(), KeyCoordinator.newSelector(p1.getId()), RequestTimeout.TRY_ONCE, tx);
			//capi.delete(getSpace().getSaalContainer(), KeyCoordinator.newSelector(p1.getId()), RequestTimeout.TRY_ONCE, tx);
			//capi.write(new Entry(p1, KeyCoordinator.newCoordinationData(p1.getId())), getSpace().getSaalContainer(), RequestTimeout.TRY_ONCE, tx);
		}
												
		// Pizza an Gruppe ausliefern.
				
		ArrayList<Serializable> gruppen = capi.take(space.getGruppeHatBestelltContainer(), Arrays.asList(KeyCoordinator.newSelector(bestellung.getGruppenId())), RequestTimeout.TRY_ONCE, tx);
		Gruppe gruppe = (Gruppe) CapiUtil.getSingleEntry(gruppen);
				
		logger.debug("Gruppe gefunden: auf Essen setzen");
		gruppe.setStatus(GruppeStatus.ESSEN);
			
		capi.write(new Entry(gruppe, KeyCoordinator.newCoordinationData(gruppe.getId())), space.getGruppeEssenContainer(), RequestTimeout.TRY_ONCE, tx);
		logger.debug("Gruppe wurde zurückgeschrieben.");
				
		bestellung.setStatus(BestellungStatus.AUSGELIEFERT);
		bestellung.setServierKellner(this.name);
			
		/* Bestellung wird in Saalcontainer geschrieben */
		List<CoordinationData> coord = Arrays.asList(KeyCoordinator.newCoordinationData(bestellung.getBestellungId()), LindaCoordinator.newCoordinationData());
		capi.write(new Entry(bestellung, coord), space.getSaalContainer(), RequestTimeout.TRY_ONCE, tx);
		capi.commitTransaction(tx);
		}
	}
	
	public void gruppeGegangen() throws MzsCoreException {

		logger.debug("gruppeGegangen()");
		
		TransactionReference tx = capi.createTransaction(2000, space.getUri());
		
		ArrayList<Gruppe> lg = capi.take(space.getGruppeGegangenContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
		if (lg == null)
			return;
		Gruppe gruppe = CapiUtil.getSingleEntry(lg);
			
		if (gruppe != null) {
				
			logger.debug("Gruppe entnommen: " + gruppe);
				
			/* Tisch der Gruppe holen */
			// Template für LindaCoordinator
			Tisch tisch = new Tisch();
			tisch.setGruppeId(gruppe.getId());
			tisch.setStatus(TischStatus.BESETZT);
				
			logger.debug("Der Tisch wird freigegeben ...");

			ArrayList<Tisch> tische = capi.take(space.getSaalContainer(), LindaCoordinator.newSelector(tisch), RequestTimeout.TRY_ONCE, tx);
			tisch = CapiUtil.getSingleEntry(tische);
				
			tisch.setGruppeId("");
			tisch.setStatus(TischStatus.FREI);
				
			List<CoordinationData> coord = Arrays.asList(LindaCoordinator.newCoordinationData(), KeyCoordinator.newCoordinationData(tisch.getId()));
			capi.write(new Entry(tisch, coord), space.getSaalContainer(), RequestTimeout.TRY_ONCE, tx);
				
			space.getCapi().commitTransaction(tx);
			logger.debug("Tisch wurde wurde in den Saal geschrieben: "+tisch);
		}
		
		logger.debug("=> Die Gruppe ist gegangen, der Tisch ist wieder frei!");
	}
	
	/**
	 * Eine Gruppe möchte bezahlen. 
	 * @param gruppeId
	 */
	public void bezahlen() throws MzsCoreException {
		
		logger.debug("Gruppe möchte bezahlen.");
		
		TransactionReference tx = capi.createTransaction(2000, space.getUri());
		ArrayList<Gruppe> gruppen = capi.take(space.getGruppeWillBezahlenContainer(), FifoCoordinator.newSelector(), RequestTimeout.TRY_ONCE, tx);
		if (gruppen == null)
			return;
		Gruppe gruppe = CapiUtil.getSingleEntry(gruppen);
		
		if (gruppe != null) {
			// Template für LindaCoordinator
			Bestellung bestTemplate = new Bestellung();
			bestTemplate.setGruppenId(gruppe.getId());
			bestTemplate.setStatus(BestellungStatus.AUSGELIEFERT);
			
			ArrayList<Bestellung> lb = capi.take(space.getSaalContainer(), LindaCoordinator.newSelector(bestTemplate), RequestTimeout.TRY_ONCE, tx);
			Bestellung best = CapiUtil.getSingleEntry(lb);
			
			if (best != null) {
				best.setBezahlKellner(this.name);
				best.setStatus(BestellungStatus.BEZAHLT);
				List<CoordinationData> coord = Arrays.asList(KeyCoordinator.newCoordinationData(best.getBestellungId()), LindaCoordinator.newCoordinationData());
				capi.write(new Entry(best, coord), space.getSaalContainer(), RequestTimeout.TRY_ONCE, tx);
				
				capi.write(new Entry(gruppe, KeyCoordinator.newCoordinationData(gruppe.getId())), space.getGruppeHatBezahltContainer(), RequestTimeout.TRY_ONCE, tx);
				capi.commitTransaction(tx);
				logger.debug("Gruppe geschrieben. "+gruppe);
			}
			else {
				logger.error("Zu zahlende Bestellung für Gruppe " + gruppe.getId() + " nicht gefunden.");
				capi.rollbackTransaction(tx);
			}
		}
		else {
			logger.warn("Keine Gruppe in GruppeBezahlen gefunden.");
			capi.rollbackTransaction(tx);
		}
	
	}
	
	@Override
	public String toString() {
		return "Kellner: ("+name+")";
	}

	/**
	 * Der Kellner wird benachrichtigt wenn: 
	 *  - neue Gruppen im Eingangscontainer sind.
	 *  - eine Bestellung im Container BestellungFertig ist
	 *  - eine Gruppe im Saal den Status ändert	 * 
	 */
	@Override
	public void entryOperationFinished(Notification notification, Operation operation,
			List<? extends Serializable> entries) {
		logger.debug("processing XVSM notification");

		String containerID = notification.getObservedContainer().getId();
		
//		System.out.println("Kellner: Notifikation erhalten "+notification.getObservedContainer().getStringRepresentation()+" : "+operation);
		logger.debug("Kellner: Notifikation erhalten "+notification.getObservedContainer().getStringRepresentation());

		try {
			
			if (containerID.equals(space.getEingangContainer().getId())) {
				logger.debug("Notification: Eingang");
				betreueEingang();
			}
			else if (containerID.equals(space.getBestellungFertigContainer().getId())) {
					bestellungAusliefern();
			}
			else if (containerID.equals(space.getGruppeWillBestellenContainer().getId())) {
				logger.debug("Notification: GruppeBestellt");
				bestellungAufnehmen();
			}
			else if (containerID.equals(space.getGruppeWillBezahlenContainer().getId())) {
				logger.debug("Notification: GruppeBezahlen");
				bezahlen();
			}
			else if (containerID.equals(space.getGruppeGegangenContainer().getId())) {
				logger.debug("Notification: GruppeGegangen");
				gruppeGegangen();
			}
			else if (containerID.equals(space.getTelefonContainer().getId())) {
				logger.debug("Notification: Telefon");
				telefonBestellungAufnehmen();
			}
			else {
				// Fehlerfall: Notifikation von einem unbekannten Container
				logger.warn("Kellner: Notifikation von einem unbekannten Container erhalten: "+notification.getObservedContainer().getStringRepresentation());
			}
		} catch (CountNotMetException | MzsTimeoutException ce) {
			logger.debug("nichts zu tun...");
		} catch (MzsCoreException me) {
			me.printStackTrace();
		}
	}

}
